Prepare the initial dataframe:
import pandas as pd
df = pd.DataFrame({
    'some_meta': [1, 2, 3, 4],
    'city': ['london', 'paris', 'London', 'moscow'],
})
df['city_lower'] = df['city'].str.lower()
df
Out[1]:
   some_meta    city city_lower
0          1  london     london
1          2   paris      paris
2          3  London     london
3          4  moscow     moscow
Create a new DataFrame with unique cities:
df_uniq_cities = df['city_lower'].drop_duplicates().to_frame()
df_uniq_cities
Out[2]:
  city_lower
0     london
1      paris
3     moscow
Run geopy's geocode on that new DataFrame:
from geopy.geocoders import Nominatim
geolocator = Nominatim(user_agent="specify_your_app_name_here")
from geopy.extra.rate_limiter import RateLimiter
geocode = RateLimiter(geolocator.geocode, min_delay_seconds=1)
df_uniq_cities['location'] = df_uniq_cities['city_lower'].apply(geocode)
# Or, instead, do this to get a nice progress bar:
# from tqdm import tqdm
# tqdm.pandas()
# df_uniq_cities['location'] = df_uniq_cities['city_lower'].progress_apply(geocode)
df_uniq_cities
Out[3]:
  city_lower                                           location
0     london  (London, Greater London, England, SW1A 2DU, UK...
1      paris  (Paris, Île-de-France, France métropolitaine, ...
3     moscow  (Москва, Центральный административный округ, М...
Merge the initial DataFrame with the new one:
df_final = pd.merge(df, df_uniq_cities, on='city_lower', how='left')
df_final['lat'] = df_final['location'].apply(lambda location: location.latitude if location is not None else None)
df_final['long'] = df_final['location'].apply(lambda location: location.longitude if location is not None else None)
df_final
Out[4]:
   some_meta    city city_lower                                           location        lat       long
0          1  london     london  (London, Greater London, England, SW1A 2DU, UK...  51.507322  -0.127647
1          2   paris      paris  (Paris, Île-de-France, France métropolitaine, ...  48.856610   2.351499
2          3  London     london  (London, Greater London, England, SW1A 2DU, UK...  51.507322  -0.127647
3          4  moscow     moscow  (Москва, Центральный административный округ, М...  55.750446  37.617494
The key to resolving your issue with timeouts is the geopy's RateLimiter class. Check out the docs for more details: https://geopy.readthedocs.io/en/1.18.1/#usage-with-pandas