First: This is THE problem in networked real-time games.
Second: To make everything “perfect,” you need to be input synchronous, and delay displaying the outcome of local player actions until the server round-trip has happened. There's no other way.
Third: If that's not acceptable, then you need to fake it somehow.
- Extrapolating other clients is helpful, because as long as they run in a straight line, you will predict them perfectly
- Turning off player/player collisions will work pretty well in practice, unless physical meelee is a thing you plan for (but then, that generally doesn't work well in this situation)
- To hide the fact that a player might collide with another player locally, you can display the other player in a displaced position, where they “avoid” the local player when they get really close
- When you get a correction from the server (if you need player collisions,) then you can apply it over a small amount of time, like 200 ms, rather than immediately; this will impact the player experience less
There are other mechanisms you can use, too, depending on the specifics of your particular gameplay. Good articles include the “source multiplayer networking article”, the “ggpo networking article”, and the “1500 archers article". (Just web search those titles, you'll find them.)